﻿// <copyright file="NetworkManager.cs" company="City of Portland, BES-ASM">
// </copyright>
// <summary>NetworkManager class</summary>

namespace BESASM.EMGAATS.UI.ArcMap
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using System.Threading.Tasks;
  using BESASM.EMGAATS.BusinessClasses;
  using BESASM.EMGAATS.DataAccess;
  using ESRI.ArcGIS.Carto;
  using ESRI.ArcGIS.Geodatabase;
  using FluentValidation.Results;

  /// <summary>
  /// A class for managing the EMGAATS network and related feature classes.
  /// </summary>
  public class NetworkManager
  {
    private Dictionary<EntityType, Dictionary<int, IEntity>> entityLookup = null;
    private Dictionary<EntityType, Dictionary<IEntity, int>> objectIdLookup = null;

    /// <summary>
    /// Gets the network being managed.
    /// </summary>
    public BusinessClasses.INetwork Network { get; private set; }

    /// <summary>
    /// Gets a value indicating whether a network has been loaded.
    /// </summary>
    public bool IsLoaded { get; private set; }

    // TODO: Set up dispose method to release feature classes, call on shutdown

    /// <summary>
    /// Gets the area feature class associated with the Network
    /// </summary>
    public IFeatureClass AreaFeatureClass { get; private set; }

    /// <summary>
    /// Gets the node feature class associated with the Network
    /// </summary>
    public IFeatureClass NodeFeatureClass { get; private set; }

    /// <summary>
    /// Gets the link feature class associated with the Network
    /// </summary>
    public IFeatureClass LinkFeatureClass { get; private set; }

    /// <summary>
    /// Gets the director feature class associated with the Network
    /// </summary>
    public IFeatureClass DirectorFeatureClass { get; private set; }

    /// <summary>
    /// Gets the cross section table associated with the Network
    /// </summary>
    public ITable CrossSectionTable { get; private set; }

    /// <summary>
    /// Gets the cross section data table associated with the Network
    /// </summary>
    public ITable CrossSectionDataTable { get; private set; }

    /// <summary>
    /// Gets the weir table associated with the Network
    /// </summary>
    public ITable WeirTable { get; private set; }

    /// <summary>
    /// Gets the orifice table associated with the Network
    /// </summary>
    public ITable OrificeTable { get; private set; }

    /// <summary>
    /// Gets the infiltrator table associated with the Network
    /// </summary>
    public ITable InfiltratorTable { get; private set; }

    /// <summary>
    /// Gets the storage table associated with the Network
    /// </summary>
    public ITable StorageTable { get; private set; }

    /// <summary>
    /// Loads the network from the given EMGAATS workspace.
    /// </summary>
    /// <param name="workspace">The workspace containing the EMGAATS network data.</param>
    /// <returns>Validation results from loading the network.</returns>
    public ValidationResult LoadNetwork(IWorkspace workspace)
    {
      this.Network = new Network();
      ModelDataAccess networkReader = new ModelDataAccess() { Workspace = workspace };
      networkReader.LoadNetwork(this.Network);
      if (networkReader.Validation == null || !networkReader.Validation.HasErrors())
      {
        this.entityLookup = networkReader.EntityLookup;
        this.objectIdLookup = this.ReverseLookup();
        this.SetFeatureClasses(workspace);
        this.IsLoaded = true;
      }
      else
      {
        this.entityLookup = null;
        this.objectIdLookup = null;
        this.ClearFeatureClasses();
        this.IsLoaded = false;
      }

      return networkReader.Validation;
    }

    /// <summary>
    /// Gets an entity for the given ObjectId
    /// </summary>
    /// <typeparam name="T">The type of entity being sought.</typeparam>
    /// <param name="objectId">The ObjectId of the feature or row storing the entity.</param>
    /// <returns>The entity for the given ObjectId.</returns>
    public T GetEntityForObjectId<T>(int objectId) where T : IEntity
    {
      if (!this.IsLoaded)
      {
        throw new InvalidOperationException("Cannot get entity because the network has not been loaded.");
      }

      IEntity entity = null;

      if (typeof(T) == typeof(INode))
      {
        entity = this.entityLookup[EntityType.Node][objectId];
      }
      else if (typeof(T) == typeof(ILink))
      {
        entity = this.entityLookup[EntityType.Link][objectId];
      }
      else if (typeof(T) == typeof(ICrossSection))
      {
        entity = this.entityLookup[EntityType.CrossSection][objectId];
      }
      else if (typeof(T) == typeof(IWeir))
      {
        entity = this.entityLookup[EntityType.Weir][objectId];
      }
      else if (typeof(T) == typeof(IOrifice))
      {
        entity = this.entityLookup[EntityType.Orifice][objectId];
      }
      else if (typeof(T) == typeof(IArea))
      {
        entity = this.entityLookup[EntityType.Area][objectId];
      }
      else if (typeof(T) == typeof(IDirector))
      {
        entity = this.entityLookup[EntityType.Director][objectId];
      }
      else if (typeof(T) == typeof(IInfiltrator))
      {
        entity = this.entityLookup[EntityType.Infiltrator][objectId];
      }
      else if (typeof(T) == typeof(IStorage))
      {
        entity = this.entityLookup[EntityType.Storage][objectId];
      }
      else
      {
        throw new ArgumentException(string.Format("Unknown entity type: {0}", typeof(T).ToString()));
      }

      return (T)entity;
    }

    /// <summary>
    /// Gets the ObjectId for a given entity.
    /// </summary>
    /// <param name="entity">The entity for which the ObjectId is sought.</param>
    /// <returns>The feature or row ObjectId for the given entity.</returns>
    public int GetObjectIdForEntity(IEntity entity)
    {
      if (!this.IsLoaded)
      {
        throw new InvalidOperationException("Cannot get ObjectId for entity because the network has not been loaded.");
      }

      if (entity == null)
      {
        throw new ArgumentNullException("entity", "Cannot get ObjectId for entity because the entity is null.");
      }

      if (entity.Network != this.Network)
      {
        throw new ArgumentException("Cannot get ObjectId because the entity does not belong to the loaded network.", "entity");
      }

      int oid = 0;
      this.objectIdLookup[entity.EntityType].TryGetValue(entity, out oid);
      return oid;
    }

    /// <summary>
    /// Gets the Feature or Row for the given entity.
    /// </summary>
    /// <param name="entity">The entity for which the Feature or Row is sought.</param>
    /// <returns>The Feature or Row for the given entity.</returns>
    public IRow GetRowForEntity(IEntity entity)
    {
      if (!this.IsLoaded)
      {
        throw new InvalidOperationException("Cannot get the row for the entity because the network has not been loaded.");
      }

      if (entity == null)
      {
        throw new ArgumentNullException("entity", "Cannot get row for entity because the entity is null.");
      }

      if (entity.Network != this.Network)
      {
        throw new ArgumentException("Cannot get row for entity because the entity does not belong to the loaded network.", "entity");
      }

      int oid = this.GetObjectIdForEntity(entity);
      switch (entity.EntityType)
      {
        case EntityType.Area:
          return this.AreaFeatureClass.GetFeature(oid);
        case EntityType.CrossSection:
          return this.CrossSectionTable.GetRow(oid);
        case EntityType.Director:
          return this.DirectorFeatureClass.GetFeature(oid);
        case EntityType.Link:
          return this.LinkFeatureClass.GetFeature(oid);
        case EntityType.Node:
          return this.NodeFeatureClass.GetFeature(oid);
        case EntityType.Weir:
          return this.WeirTable.GetRow(oid);
        case EntityType.Orifice:
          return this.OrificeTable.GetRow(oid);
        case EntityType.Infiltrator:
          return this.InfiltratorTable.GetRow(oid);
        case EntityType.Storage:
          return this.StorageTable.GetRow(oid);
        default:
          return null;
      }
    }

    /// <summary>
    /// Determines what type of entity is stored in the given object class (table/feature class).
    /// </summary>
    /// <param name="objectClass">The table or feature class to check.</param>
    /// <returns>The entity type of the table or feature class or Unknown if not an entity layer or it can't be determined.</returns>
    public EntityType GetObjectClassEntityType(IObjectClass objectClass)
    {
      if (!this.IsLoaded)
      {
        throw new InvalidOperationException("Cannot get the entity type for the object class because the network has not been loaded.");
      }

      if (objectClass == null)
      {
        return EntityType.Unknown;
      }
      else if (objectClass == this.AreaFeatureClass)
      {
        return EntityType.Area;
      }
      else if (objectClass == this.CrossSectionTable)
      {
        return EntityType.CrossSection;
      }
      else if (objectClass == this.DirectorFeatureClass)
      {
        return EntityType.Director;
      }
      else if (objectClass == this.LinkFeatureClass)
      {
        return EntityType.Link;
      }
      else if (objectClass == this.NodeFeatureClass)
      {
        return EntityType.Node;
      }
      else if (objectClass == this.WeirTable)
      {
        return EntityType.Weir;
      }
      else if (objectClass == this.OrificeTable)
      {
        return EntityType.Orifice;
      }
      else if (objectClass == this.InfiltratorTable)
      {
        return EntityType.Infiltrator;
      }
      else if (objectClass == this.StorageTable)
      {
        return EntityType.Storage;
      }
      else
      {
        return EntityType.Unknown;
      }
    }

    /// <summary>
    /// Gets the entity corresponding to the given geodatabase object (a row or feature).
    /// </summary>
    /// <param name="entityObject">The geodatabase object (a row or feature) representing the entity.</param>
    /// <returns>The entity corresponding to the given geodatabase object (a row or feature).</returns>
    public IEntity GetEntityForObject(IObject entityObject)
    {
      if (!this.IsLoaded)
      {
        throw new InvalidOperationException("Cannot get the entity for the object because the network has not been loaded.");
      }

      if (entityObject != null && entityObject.Class != null)
      {
        EntityType entityType = this.GetObjectClassEntityType(entityObject.Class);
        switch (entityType)
        {
          case EntityType.Area:
            return this.GetEntityForObjectId<IArea>(entityObject.OID);
          case EntityType.CrossSection:
            return this.GetEntityForObjectId<ICrossSection>(entityObject.OID);
          case EntityType.Director:
            return this.GetEntityForObjectId<IDirector>(entityObject.OID);
          case EntityType.Link:
            return this.GetEntityForObjectId<ILink>(entityObject.OID);
          case EntityType.Node:
            return this.GetEntityForObjectId<INode>(entityObject.OID);
          case EntityType.Weir:
            return this.GetEntityForObjectId<IWeir>(entityObject.OID);
          case EntityType.Orifice:
            return this.GetEntityForObjectId<IOrifice>(entityObject.OID);
          case EntityType.Infiltrator:
            return this.GetEntityForObjectId<IInfiltrator>(entityObject.OID);
          case EntityType.Storage:
            return this.GetEntityForObjectId<IStorage>(entityObject.OID);
        }
      }

      return null;
    }

    /// <summary>
    /// Retrieves the feature class or table corresponding to the given entity type.
    /// </summary>
    /// <param name="entityType">The entity type being sought.</param>
    /// <returns>The feature class or table corresponding to the given entity type.</returns>
    public IObjectClass GetObjectClassForEntityType(EntityType entityType)
    {
      if (!this.IsLoaded)
      {
        throw new InvalidOperationException("Cannot get the object class for the entity type because the network has not been loaded.");
      }

      switch (entityType)
      {
        case EntityType.Area:
          return this.AreaFeatureClass;
        case EntityType.CrossSection:
          return this.CrossSectionTable as IObjectClass;
        case EntityType.Director:
          return this.DirectorFeatureClass;
        case EntityType.Link:
          return this.LinkFeatureClass;
        case EntityType.Node:
          return this.NodeFeatureClass;
        case EntityType.Weir:
          return this.WeirTable as IObjectClass;
        case EntityType.Orifice:
          return this.OrificeTable as IObjectClass;
        case EntityType.Infiltrator:
          return this.InfiltratorTable as IObjectClass;
        case EntityType.Storage:
          return this.StorageTable as IObjectClass;
        default:
          return null;
      }
    }

    private void SetFeatureClasses(IWorkspace workspace)
    {
      DataSchema schema = new DataSchema();

      IFeatureWorkspace featureWorkspace = (IFeatureWorkspace)workspace;
      this.AreaFeatureClass = featureWorkspace.OpenFeatureClass(schema.Areas.TableName);
      this.CrossSectionDataTable = featureWorkspace.OpenTable(schema.CrossSectionData.TableName);
      this.CrossSectionTable = featureWorkspace.OpenTable(schema.CrossSections.TableName);
      this.DirectorFeatureClass = featureWorkspace.OpenFeatureClass(schema.Directors.TableName);
      this.LinkFeatureClass = featureWorkspace.OpenFeatureClass(schema.Links.TableName);
      this.NodeFeatureClass = featureWorkspace.OpenFeatureClass(schema.Nodes.TableName);
      this.WeirTable = featureWorkspace.OpenTable(schema.Weirs.TableName);
      this.OrificeTable = featureWorkspace.OpenTable(schema.Orifices.TableName);
      this.InfiltratorTable = featureWorkspace.OpenTable(schema.Infiltrators.TableName);
      this.StorageTable = featureWorkspace.OpenTable(schema.Storages.TableName);
    }

    private void ClearFeatureClasses()
    {
      this.AreaFeatureClass = null;
      this.CrossSectionDataTable = null;
      this.CrossSectionTable = null;
      this.DirectorFeatureClass = null;
      this.LinkFeatureClass = null;
      this.NodeFeatureClass = null;
      this.WeirTable = null;
      this.OrificeTable = null;
      this.InfiltratorTable = null;
      this.StorageTable = null;
    }

    private Dictionary<EntityType, Dictionary<IEntity, int>> ReverseLookup()
    {
      Dictionary<EntityType, Dictionary<IEntity, int>> reverseLookup = new Dictionary<EntityType, Dictionary<IEntity, int>>();
      foreach (EntityType entityType in this.entityLookup.Keys)
      {
        Dictionary<IEntity, int> idLookup = new Dictionary<IEntity, int>();
        reverseLookup[entityType] = idLookup;
        foreach (var dictionaryPair in this.entityLookup[entityType])
        {
          idLookup.Add(dictionaryPair.Value, dictionaryPair.Key);
        }
      }

      return reverseLookup;
    }
  }
}
